#!/usr/bin/env python

import traceback
import pprint
import sys
import os
import optparse
import ensemblerobot as erobot
from midi import *
from midi import sequencer


class Diagnostics(object):
    _commands = {
      'h':('cmd_help', 'print command help'),
      'dump':('cmd_toggle_textdump', 'toggle the track textdump flag'),
      's':('cmd_sweep_keys', 'sweep pitch range with current delay in ticks'),
      'on':('cmd_noteon', 'send a raw note on without translation or noteoff'),
      'off':('cmd_noteoff', 'send a raw note off without translation'),
      'channel':('set_channel', 'set the MIDI channel'),
      'pitch':('set_pitch', 'set the MIDI pitch'),
      'n':('cmd_inc_pitch', 'increment pitch'),
      'p':('cmd_dec_pitch', 'decrement pitch'),
      'd':('cmd_set_delay', 'set the delay length in ticks'),
      '+':('cmd_inc_delay', 'increase the delay length by one tick'),
      '-':('cmd_dec_delay', 'decrease the delay length by one tick'),
      'r':('cmd_repeat', 'repeat a note # number of times'),
      't':('cmd_translate', 'translate modes'),
      # heliphon commands
      'l':('cmd_heliphon_low', 'set heliphon low timing for note in delaymap'),
      'h':('cmd_heliphon_high', 'set heliphon high timing for note in delaymap'),
      'v':('cmd_heliphon_velocity', 'heliphon velocity audition'),
      'dumpdmap':('cmd_heliphon_dump_delaymap', 'dump the heliphon delaymap'),
      'resetdmap':('cmd_heliphon_reset_delaymap', 'reset heliphon delaymap with ow high'),
      'z':('cmd_heliphon_sweep', 'sweep heliphonic range'),
      'w':('cmd_heliphon_warmup', 'warmup heliphon'),
    }

    def cmd_heliphon_dump_delaymap(self, *args):
        dmap = \
"""    'delaymap':{
%s
    },
"""
        dtable = [] 
        for pitch in range(G_3, Gs_5):
            low, high = self.heliphon.delaymap[pitch]
            pname = NOTE_VALUE_MAP_SHARP[pitch]
            line = "        midi.%s: (%f, %f)," % (pname, low, high)
            dtable.append(line)
        dmap = dmap % str.join('\n', dtable)
        print dmap
        dmapf = open('delay.map', 'w')
        dmapf.write(dmap)
        dmapf.close()
            
    def cmd_heliphon_reset_delaymap(self, *args):
        try:
            low = int(args[0])
            high = int(args[1])
        except:
            print "resetdmap <low-value> <high-value>"
        low = self._delay_to_sec(low)
        high = self._delay_to_sec(high)
        for pitch in range(G_3, Gs_5):
            self.heliphon.delaymap[pitch] = (low, high)

    def __init__(self, options):
        self.config = erobot.Configuration.get_instance()
        self.pitch = G_3
        self.channel = 0
        self.delay = 1
        self.tempo = 240
        self.resolution = 1000
        self.velocity = 1
        self.textdump = False
        self.translate = None
        self.heliphon = erobot.Heliphon()
        args = {
            'sequencer_resolution':self.resolution,
            'sequencer_tempo':self.tempo
        }
        self.sequencer = sequencer.SequencerWrite(**args)
        print "Opening port %d:%d" % (options.client, options.port)
        if self.sequencer.get_nonblock():
            print "blocking sequencer, setting to non-blocking."
            self.sequencer.set_nonblock(False)
        else:
            print "Sequencer set to non-blocking."
        self.sequencer.subscribe_port(options.client, options.port)
        ## ensemble robot configuration
        self.configfn = options.configfn
        self.config = erobot.Configuration.get_instance()
        #self.cmd_confload()

    def cmd_translate(self, *args):
        if not len(args):
            print "Translation reset"
            self.translate = None
        else:
            if args[0] == 'heliphon':
                print "Heliphonic translation set"
                self.translate = 'heliphon'
            else:
                print "Unknown translation mode, must be 'heliphon' or None."
        return False

    def cmd_repeat(self, *args):
        try:
            r = int(args[0])
        except:
            print "You must enter the number of repeats"
            return False
        stream = self._new_stream()
        tick = 0
        for x in range(r):
            if self.translate == 'heliphon':
                spitch = self.heliphon.translate_solenoid(self.pitch)
                lpitch = self.heliphon.translate_led(self.pitch)
                self._notepair(stream, spitch, tick, self.delay, 
                                        channel=self.heliphon.SolenoidChannel)
                self._notepair(stream, lpitch, tick, self.delay, 
                                        channel=self.heliphon.LEDChannel)
            else:
                self._notepair(stream, self.pitch, tick, self.delay, self.channel)
            tick += self.delay * 2
        self._play(stream)
            
    def cmd_heliphon_warmup(self, *args):
        stream = self._new_stream()
        tick = 0
        for delay in range(10, 200, 10):
            for pitch in range(G_3, Gs_5):
                spitch = self.heliphon.translate_solenoid(pitch)
                lpitch = self.heliphon.translate_led(pitch)
                self._notepair(stream, spitch, tick, delay, 
                                        channel=self.heliphon.SolenoidChannel)
                self._notepair(stream, lpitch, tick, delay, 
                                        channel=self.heliphon.LEDChannel)
                tick += delay
            tick += delay
        self._play(stream)
        return False

    def cmd_heliphon_sweep(self, *args):
        tick = 0
        stream = self._new_stream()
        for pitch in range(G_3, Gs_5):
            spitch = self.heliphon.translate_solenoid(pitch)
            lpitch = self.heliphon.translate_led(pitch)
            self._notepair(stream, spitch, tick, self.delay, 
                                    channel=self.heliphon.SolenoidChannel)
            self._notepair(stream, lpitch, tick, self.delay, 
                                    channel=self.heliphon.LEDChannel)
            tick += self.delay
        self._play(stream)
        return False

    def cmd_heliphon_low(self, *args):
        low, high = self.heliphon.delaymap[self.pitch]
        low = self._delay_to_sec()
        self.heliphon.delaymap[self.pitch] = low, high

    def cmd_heliphon_high(self, *args):
        low, high = self.heliphon.delaymap[self.pitch]
        high = self._delay_to_sec()
        self.heliphon.delaymap[self.pitch] = low, high

    def cmd_heliphon_velocity(self, *args):
        try:
            velocity = max(min(int(args[0]), 0x7f), 0)
        except:
            print "Velocity value must be between 0 <= v <= 127"
            return False
        try:
            low, high = self.heliphon.delaymap[self.pitch]
        except KeyError:
            low, high = self.heliphon.delaymap[G_3]
        delay_in_sec = low + (high - low) * (velocity / float(0x7f))
        beats_per_sec = self.tempo / 60.0
        beats_per_delay = beats_per_sec * delay_in_sec
        self.delay = int(beats_per_delay * self.resolution)
        return True

    ## command parser
    def prompt(self):
        print "[Pitch:%s] [Delay: %s ticks / %f s] [Tempo:%d] [Resolution:%s]" % ( \
                    self.pitch,
                    self.delay,
                    self._delay_to_sec(),
                    self.tempo,
                    self.resolution
        )
        cmdstr = raw_input( '> ' )
        self.command(cmdstr)

    def command(self, cmdstr):
        args = cmdstr.split(' ')
        cmd = args[0]
        args = args[1:]
        default = True
        if cmd:
            if cmd not in self._commands:
                print "Unknown command: %s" % cmd
                default = self.cmd_help(*args)
            else:
                fn = self._commands[cmd][0]
                func = getattr(self, fn)
                default = func(*args)
        if default:
            self._cmd_default()
        print

    def parse_pitch(self, val):
        try:
            pitch = int( args[0] )
        except ValueError:
            try:
                pitch = eval(str(args[0]))
            except:
                raise ValueError, "Unknown pitch value: %s" % pitch

    ## command code
    def cmd_help(self, *args):
        def _sort(arg1, arg2):
            if len(arg1) < len(arg2):
                return -1
            elif len(arg1) > len(arg2):
                return 1
            else:
                if arg1 > arg2:
                    return -1
                elif arg1 < arg2:
                    return 1
            return 0
        commands = self._commands.keys()
        commands.sort(_sort)
        for command in commands:
            print "%s - %s" % (command, self._commands[command][1])
        return False

    def cmd_toggle_textdump(self, *args):
        if self.textdump:
            print "enabling track textdumps."
        else:
            print "enabling track textdumps."
        self.textdump = not self.textdump

    def cmd_set_delay(self, *args):
        if len(args):
            self.delay = int(args[0])
        return True

    def cmd_inc_pitch(self, *args):
        self.pitch += 1
        return True

    def cmd_dec_pitch(self, *args):
        self.pitch -= 1
        return True

    def cmd_inc_delay(self, *args):
        self.delay += 1
        return True

    def cmd_dec_delay(self, *args):
        self.delay -= 1
        return True

    def cmd_sweep_keys(self, *args):
        tick = 0
        #delay = int(self.resolution / 50.0)
        stream = self._new_stream()
        for pitch in range(0x7f):
            self._notepair(stream, pitch, tick, self.delay, channel=self.channel)
            tick += self.delay
        self._play(stream)
        return False

    def cmd_noteon(self, *args):
        stream = self._new_stream()
        self._noteon(stream, self.pitch, 0, channel=self.channel)
        self._play(stream)
        return False

    def cmd_noteoff(self, *args):
        stream = self._new_stream()
        self._noteoff(stream, self.pitch, 0, channel=self.channel)
        self._play(stream)
        return False

    ## default command
    def _cmd_default(self):
        stream = self._new_stream()
        if self.translate == 'heliphon':
            spitch = self.heliphon.translate_solenoid(self.pitch)
            lpitch = self.heliphon.translate_led(self.pitch)
            print "delay: %d (%f s)" % (self.delay, self._convert_delay())
            print "solenoid: %s -> %s" % (self.pitch, spitch)
            print "light: %s -> %s" % (self.pitch, lpitch)
            self._notepair(stream, spitch, 0, self.delay, 
                                    channel=self.heliphon.SolenoidChannel)
            self._notepair(stream, lpitch, 0, self.delay, 
                                    channel=self.heliphon.LEDChannel)
        else:
            # no translation
            self._notepair(stream, self.pitch, 0, self.delay, channel=self.channel)
        self._play(stream)

    def set_channel(self, *args):
        self.channel = eval(args[0])
        print "Channel set to %d" % self.channel
        return False

    def set_pitch(self, *args):
        self.pitch = eval(args[0])
        print "Pitch set to %d" % self.pitch
        return False

    ## support
    def _new_stream(self):
        stream = EventStream()
        stream.add_track()
        return stream

    def _delay_to_sec(self, delay=None):
        if not delay:
            delay = self.delay
        spb = 1.0 / (self.tempo / 60.0)
        return (delay * spb) / self.resolution
            
    def _noteon(self, stream, pitch, tick, channel=0, velocity=0x7f):
        ev = NoteOnEvent()
        ev.velocity = velocity
        ev.pitch = pitch
        ev.tick = tick
        ev.channel = channel
        stream.add_event(ev)

    def _noteoff(self, stream, pitch, tick, channel=0):
        ev = NoteOffEvent()
        ev.velocity = 0
        ev.pitch = pitch
        ev.tick = tick
        ev.channel = channel
        stream.add_event(ev)

    def _notepair(self, stream, pitch, tick, delay, channel=0, velocity=0x7f):
        self._noteon(stream, pitch, tick, channel, velocity)
        self._noteoff(stream, pitch, tick+delay, channel)

    def _play(self, stream):
        self.sequencer.stop_sequencer()
        self.sequencer.start_sequencer()
        if self.textdump:
            stream.textdump()
        for event in stream.iterevents():
            self.sequencer.event_write(event, tick=True)

    def _convert_delay(self):
        beats_per_second = self.tempo / 60.0
        ticks_per_second = beats_per_second * self.resolution
        return self.delay / ticks_per_second

def parse_options():
    def cp_split(option, optstr, value, parser):
        if ":" in value:
            client_port = str.split(value, ":")
            if len(client_port) != 2:
                raise ValueError, "-p must be in client:port format ex. 72:0"
            client = int(client_port[0])
            port = int(client_port[1])
        setattr(parser.values, "client", client)
        setattr(parser.values, "port", port)

    parser = optparse.OptionParser()
    parser.add_option("-p", "--port",
                        action="callback",
                        callback=cp_split,
                        type="string",
                        help="client:port")
    parser.add_option("-c", "--conf",
                        action="store",
                        type="string",
                        dest="configfn",
                        help="Configuration file")
    parser.set_defaults(configfn="erobot.conf",port=0,client=72)
    options, extra = parser.parse_args()
    if extra:
        parser.print_help()
        sys.exit(1)
    return options

if __name__ == '__main__':
    opts = parse_options()
    diagnostics = Diagnostics(opts)
    while True:
        try:
            diagnostics.prompt()
        except (EOFError, KeyboardInterrupt):
            break
        except:
            print "Runtime Exception ::"
            traceback.print_exc()

